]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
Add workspace thumbnails to the overview
authorOwen W. Taylor <otaylor@fishsoup.net>
Mon, 31 Jan 2011 02:18:12 +0000 (21:18 -0500)
committerOwen W. Taylor <otaylor@fishsoup.net>
Wed, 9 Feb 2011 00:43:20 +0000 (19:43 -0500)
Add workspace thumbnails to the workspace controls area. The user can
click on the thumbnail to switch workspaces and can also drag windows
out of the thumbnail to other workspaces.

https://bugzilla.gnome.org/show_bug.cgi?id=640996

data/theme/gnome-shell.css
js/Makefile.am
js/ui/workspaceThumbnail.js [new file with mode: 0644]
js/ui/workspacesView.js

index b0dda27d3e3dc12374e5fd719a601a91d39646d9..bbe76a7e211b71c9e23113ef1c1a368175c1f61b 100644 (file)
@@ -261,21 +261,29 @@ StTooltip StLabel {
 }
 
 .workspace-controls {
-    width: 48px;
     font-size: 32px;
     font-weight: bold;
     color: #ffffff;
-    border: 2px solid rgba(128, 128, 128, 0.4);
+    border: 1px solid #424242;
     border-right: 0px;
     border-radius: 9px 0px 0px 9px;
+    background: #071524;
+}
+
+.workspace-thumbnails {
+    spacing: 7px;
+    padding: 8px;
+}
+
+.workspace-thumbnail-indicator {
+    outline: 2px solid white;
 }
 
 .add-workspace {
-    background-color: rgba(128, 128, 128, 0.4);
 }
 
 .add-workspace:hover {
-    background-color: rgba(128, 128, 128, 0.6);
+    background-color: rgba(128, 128, 128, 0.2);
 }
 
 .remove-workspace {
index 21683af19a61e5ad54e6ffc95bcc0e681ab91360..955c308ff0bb7b08761f14915604fec9a97e2493 100644 (file)
@@ -58,6 +58,7 @@ nobase_dist_js_DATA =         \
        ui/windowAttentionHandler.js    \
        ui/windowManager.js     \
        ui/workspace.js         \
+       ui/workspaceThumbnail.js        \
        ui/workspacesView.js    \
        ui/workspaceSwitcherPopup.js    \
        ui/xdndHandler.js
diff --git a/js/ui/workspaceThumbnail.js b/js/ui/workspaceThumbnail.js
new file mode 100644 (file)
index 0000000..8f8586b
--- /dev/null
@@ -0,0 +1,295 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Clutter = imports.gi.Clutter;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const Shell = imports.gi.Shell;
+const Signals = imports.signals;
+const St = imports.gi.St;
+
+const DND = imports.ui.dnd;
+const Main = imports.ui.main;
+const Workspace = imports.ui.workspace;
+
+// Fraction of original screen size for thumbnails
+let THUMBNAIL_SCALE = 1/8.;
+
+function WindowClone(realWindow) {
+    this._init(realWindow);
+}
+
+WindowClone.prototype = {
+    _init : function(realWindow) {
+        this.actor = new Clutter.Clone({ source: realWindow.get_texture(),
+                                         reactive: true,
+                                         x: realWindow.x,
+                                         y: realWindow.y });
+        this.actor._delegate = this;
+        this.realWindow = realWindow;
+        this.metaWindow = realWindow.meta_window;
+
+        this.actor.connect('button-release-event',
+                           Lang.bind(this, this._onButtonRelease));
+
+        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+
+        this._draggable = DND.makeDraggable(this.actor,
+                                            { restoreOnSuccess: true,
+                                              dragActorMaxSize: Workspace.WINDOW_DND_SIZE,
+                                              dragActorOpacity: Workspace.DRAGGING_WINDOW_OPACITY });
+        this._draggable.connect('drag-begin', Lang.bind(this, this._onDragBegin));
+        this._draggable.connect('drag-end', Lang.bind(this, this._onDragEnd));
+        this.inDrag = false;
+
+        this._selected = false;
+    },
+
+    setStackAbove: function (actor) {
+        this._stackAbove = actor;
+        if (this._stackAbove == null)
+            this.actor.lower_bottom();
+        else
+            this.actor.raise(this._stackAbove);
+    },
+
+    destroy: function () {
+        this.actor.destroy();
+    },
+
+    _onDestroy: function() {
+        this.actor._delegate = null;
+
+        if (this.inDrag) {
+            this.emit('drag-end');
+            this.inDrag = false;
+        }
+
+        this.disconnectAll();
+    },
+
+    _onButtonRelease : function (actor, event) {
+        this._selected = true;
+        this.emit('selected', event.get_time());
+    },
+
+    _onDragBegin : function (draggable, time) {
+        this.inDrag = true;
+        this.emit('drag-begin');
+    },
+
+    _onDragEnd : function (draggable, time, snapback) {
+        this.inDrag = false;
+
+        // We may not have a parent if DnD completed successfully, in
+        // which case our clone will shortly be destroyed and replaced
+        // with a new one on the target workspace.
+        if (this.actor.get_parent() != null) {
+            if (this._stackAbove == null)
+                this.actor.lower_bottom();
+            else
+                this.actor.raise(this._stackAbove);
+        }
+
+
+        this.emit('drag-end');
+    }
+};
+Signals.addSignalMethods(WindowClone.prototype);
+
+
+/**
+ * @metaWorkspace: a #Meta.Workspace
+ */
+function WorkspaceThumbnail(metaWorkspace) {
+    this._init(metaWorkspace);
+}
+
+WorkspaceThumbnail.prototype = {
+    _init : function(metaWorkspace) {
+        this.metaWorkspace = metaWorkspace;
+
+        this.actor = new St.Bin({ reactive: true,
+                                  clip_to_allocation: true,
+                                  style_class: 'workspace-thumbnail' });
+        this.actor._delegate = this;
+
+        this._group = new Clutter.Group();
+        this.actor.add_actor(this._group);
+
+        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+        this.actor.connect('button-press-event', Lang.bind(this,
+            function(actor, event) {
+                return true;
+            }));
+        this.actor.connect('button-release-event', Lang.bind(this,
+            function(actor, event) {
+                this.metaWorkspace.activate(event.get_time());
+                return true;
+            }));
+
+        this._background = new Clutter.Clone({ source: global.background_actor });
+        this._group.add_actor(this._background);
+
+        this._group.set_size(THUMBNAIL_SCALE * global.screen_width, THUMBNAIL_SCALE * global.screen_height);
+        this._group.set_scale(THUMBNAIL_SCALE, THUMBNAIL_SCALE);
+
+        let windows = global.get_window_actors().filter(this._isMyWindow, this);
+
+        // Create clones for windows that should be visible in the Overview
+        this._windows = [];
+        for (let i = 0; i < windows.length; i++) {
+            if (this._isOverviewWindow(windows[i])) {
+                this._addWindowClone(windows[i]);
+            }
+        }
+
+        // Track window changes
+        this._windowAddedId = this.metaWorkspace.connect('window-added',
+                                                          Lang.bind(this, this._windowAdded));
+        this._windowRemovedId = this.metaWorkspace.connect('window-removed',
+                                                           Lang.bind(this, this._windowRemoved));
+    },
+
+    _lookupIndex: function (metaWindow) {
+        for (let i = 0; i < this._windows.length; i++) {
+            if (this._windows[i].metaWindow == metaWindow) {
+                return i;
+            }
+        }
+        return -1;
+    },
+
+    syncStacking: function(stackIndices) {
+        this._windows.sort(function (a, b) { return stackIndices[a.metaWindow.get_stable_sequence()] - stackIndices[b.metaWindow.get_stable_sequence()]; });
+
+        for (let i = 0; i < this._windows.length; i++) {
+            let clone = this._windows[i];
+            let metaWindow = clone.metaWindow;
+            if (i == 0) {
+                clone.setStackAbove(this._background);
+            } else {
+                let previousClone = this._windows[i - 1];
+                clone.setStackAbove(previousClone.actor);
+            }
+        }
+    },
+
+    _windowRemoved : function(metaWorkspace, metaWin) {
+        let win = metaWin.get_compositor_private();
+
+        // find the position of the window in our list
+        let index = this._lookupIndex (metaWin);
+
+        if (index == -1)
+            return;
+
+        let clone = this._windows[index];
+        this._windows.splice(index, 1);
+        clone.destroy();
+    },
+
+    _windowAdded : function(metaWorkspace, metaWin) {
+        if (this.leavingOverview)
+            return;
+
+        let win = metaWin.get_compositor_private();
+
+        if (!win) {
+            // Newly-created windows are added to a workspace before
+            // the compositor finds out about them...
+            Mainloop.idle_add(Lang.bind(this,
+                                        function () {
+                                            if (this.actor && metaWin.get_compositor_private())
+                                                this._windowAdded(metaWorkspace, metaWin);
+                                            return false;
+                                        }));
+            return;
+        }
+
+        if (!this._isOverviewWindow(win))
+            return;
+
+        let clone = this._addWindowClone(win);
+    },
+
+    destroy : function() {
+        this.actor.destroy();
+    },
+
+    _onDestroy: function(actor) {
+        this.metaWorkspace.disconnect(this._windowAddedId);
+        this.metaWorkspace.disconnect(this._windowRemovedId);
+
+        this._windows = [];
+        this.actor = null;
+    },
+
+    // Tests if @win belongs to this workspaces
+    _isMyWindow : function (win) {
+        return win.get_workspace() == this.metaWorkspace.index() ||
+            (win.get_meta_window() && win.get_meta_window().is_on_all_workspaces());
+    },
+
+    // Tests if @win should be shown in the Overview
+    _isOverviewWindow : function (win) {
+        let tracker = Shell.WindowTracker.get_default();
+        return tracker.is_window_interesting(win.get_meta_window());
+    },
+
+    // Create a clone of a (non-desktop) window and add it to the window list
+    _addWindowClone : function(win) {
+        let clone = new WindowClone(win);
+
+        clone.connect('selected',
+                      Lang.bind(this, this._onCloneSelected));
+        clone.connect('drag-begin',
+                      Lang.bind(this, function(clone) {
+                          Main.overview.beginWindowDrag();
+                      }));
+        clone.connect('drag-end',
+                      Lang.bind(this, function(clone) {
+                          Main.overview.endWindowDrag();
+                      }));
+        this._group.add_actor(clone.actor);
+
+        this._windows.push(clone);
+
+        return clone;
+    },
+
+    _onCloneSelected : function (clone, time) {
+        this.metaWorkspace.activate(time);
+    },
+
+    // Draggable target interface
+    handleDragOver : function(source, actor, x, y, time) {
+        if (source.realWindow)
+            return DND.DragMotionResult.MOVE_DROP;
+        if (source.shellWorkspaceLaunch)
+            return DND.DragMotionResult.COPY_DROP;
+
+        return DND.DragMotionResult.CONTINUE;
+    },
+
+    acceptDrop : function(source, actor, x, y, time) {
+        if (source.realWindow) {
+            let win = source.realWindow;
+            if (this._isMyWindow(win))
+                return false;
+
+            let metaWindow = win.get_meta_window();
+            metaWindow.change_workspace_by_index(this.metaWorkspace.index(),
+                                                 false, // don't create workspace
+                                                 time);
+            return true;
+        } else if (source.shellWorkspaceLaunch) {
+            this.metaWorkspace.activate(time);
+            source.shellWorkspaceLaunch();
+            return true;
+        }
+
+        return false;
+    }
+};
+
+Signals.addSignalMethods(WorkspaceThumbnail.prototype);
index 8f97d0ee988765d675fc6705e1278e2db093bcd1..5e24141b3ceb848adcf2da07a270364664ce6866 100644 (file)
@@ -14,6 +14,7 @@ const Main = imports.ui.main;
 const Overview = imports.ui.overview;
 const Tweener = imports.ui.tweener;
 const Workspace = imports.ui.workspace;
+const WorkspaceThumbnail = imports.ui.workspaceThumbnail;
 
 const WORKSPACE_SWITCH_TIME = 0.25;
 // Note that mutter has a compile-time limit of 36
@@ -725,6 +726,25 @@ WorkspacesDisplay.prototype = {
         }));
         controls.add(this._removeButton);
 
+        this._thumbnailsBox = new St.BoxLayout({ vertical: true,
+                                                 style_class: 'workspace-thumbnails' });
+        controls.add(this._thumbnailsBox, { expand: false });
+
+        let indicator = new St.Bin({ style_class: 'workspace-thumbnail-indicator',
+                                     fixed_position_set: true });
+
+        // We don't want the indicator to affect drag-and-drop
+        Shell.util_set_hidden_from_pick(indicator, true);
+
+        this._thumbnailIndicator = indicator;
+        this._thumbnailsBox.add(this._thumbnailIndicator);
+        this._thumbnailIndicatorConstraints = [];
+        this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.POSITION }));
+        this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.SIZE }));
+        this._thumbnailIndicatorConstraints.forEach(function(constraint) {
+                                                        indicator.add_constraint(constraint);
+                                                    });
+
         this._addButton = new St.Button({ label: '+',
                                           style_class: 'add-workspace' });
         this._addButton.connect('clicked', Lang.bind(this, function() {
@@ -759,11 +779,22 @@ WorkspacesDisplay.prototype = {
         this._controls.show();
 
         this._workspaces = [];
+        this._workspaceThumbnails = [];
         for (let i = 0; i < global.screen.n_workspaces; i++) {
             let metaWorkspace = global.screen.get_workspace_by_index(i);
             this._workspaces[i] = new Workspace.Workspace(metaWorkspace);
+
+            let thumbnail = new WorkspaceThumbnail.WorkspaceThumbnail(metaWorkspace);
+            this._workspaceThumbnails[i] = thumbnail;
+            this._thumbnailsBox.add(thumbnail.actor);
         }
 
+        // The thumbnails indicator actually needs to be on top of the thumbnails, but
+        // there is also something more subtle going on as well - actors in a StBoxLayout
+        // are allocated from bottom to top (start to end), and we need the
+        // thumnail indicator to be allocated after the actors it is constrained to.
+        this._thumbnailIndicator.raise_top();
+
         let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL);
 
         let totalAllocation = this.actor.allocation;
@@ -810,6 +841,9 @@ WorkspacesDisplay.prototype = {
         this._nWorkspacesNotifyId =
             global.screen.connect('notify::n-workspaces',
                                   Lang.bind(this, this._workspacesChanged));
+        this._switchWorkspaceNotifyId =
+            global.window_manager.connect('switch-workspace',
+                                          Lang.bind(this, this._activeWorkspaceChanged));
 
         this._restackedNotifyId =
             global.screen.connect('restacked',
@@ -829,6 +863,9 @@ WorkspacesDisplay.prototype = {
                                                           Lang.bind(this, this._dragEnd));
 
         this._onRestacked();
+        this._constrainThumbnailIndicator();
+        this._zoomOut = false;
+        this._updateZoom();
     },
 
     hide: function() {
@@ -838,6 +875,10 @@ WorkspacesDisplay.prototype = {
             global.screen.disconnect(this._nWorkspacesNotifyId);
             this._nWorkspacesNotifyId = 0;
         }
+        if (this._switchWorkspaceNotifyId > 0) {
+            global.window_manager.disconnect(this._switchWorkspaceNotifyId);
+            this._switchWorkspaceNotifyId = 0;
+        }
         if (this._restackedNotifyId > 0){
             global.screen.disconnect(this._restackedNotifyId);
             this._restackedNotifyId = 0;
@@ -861,12 +902,51 @@ WorkspacesDisplay.prototype = {
 
         this.workspacesView.destroy();
         this.workspacesView = null;
+        this._unconstrainThumbnailIndicator();
         for (let w = 0; w < this._workspaces.length; w++) {
             this._workspaces[w].disconnectAll();
             this._workspaces[w].destroy();
+            this._workspaceThumbnails[w].destroy();
         }
     },
 
+    _constrainThumbnailIndicator: function() {
+        let active = global.screen.get_active_workspace_index();
+        let thumbnail = this._workspaceThumbnails[active];
+
+        this._thumbnailIndicatorConstraints.forEach(function(constraint) {
+                                                        constraint.set_source(thumbnail.actor);
+                                                        constraint.set_enabled(true);
+                                                    });
+    },
+
+    _unconstrainThumbnailIndicator: function() {
+        this._thumbnailIndicatorConstraints.forEach(function(constraint) {
+                                                        constraint.set_enabled(false);
+                                                    });
+    },
+
+    _activeWorkspaceChanged: function(wm, from, to, direction) {
+        let active = global.screen.get_active_workspace_index();
+        let thumbnail = this._workspaceThumbnails[active];
+
+        this._unconstrainThumbnailIndicator();
+        let oldAllocation = this._thumbnailIndicator.allocation;
+        this._thumbnailIndicator.x = oldAllocation.x1;
+        this._thumbnailIndicator.y = oldAllocation.y1;
+        this._thumbnailIndicator.width = oldAllocation.x2 - oldAllocation.x1;
+        this._thumbnailIndicator.height = oldAllocation.y2 - oldAllocation.y1;
+
+        Tweener.addTween(this._thumbnailIndicator,
+                         { x: thumbnail.actor.allocation.x1,
+                           y: thumbnail.actor.allocation.y1,
+                           time: WORKSPACE_SWITCH_TIME,
+                           transition: 'easeOutQuad',
+                           onComplete: Lang.bind(this,
+                                                 this._constrainThumbnailIndicator)
+                         });
+    },
+
     _onRestacked: function() {
         let stack = global.get_window_actors();
         let stackIndices = {};
@@ -877,6 +957,8 @@ WorkspacesDisplay.prototype = {
         }
 
         this.workspacesView.syncStacking(stackIndices);
+        for (let i = 0; i < this._workspaceThumbnails.length; i++)
+            this._workspaceThumbnails[i].syncStacking(stackIndices);
     },
 
     _workspacesChanged: function() {
@@ -893,7 +975,12 @@ WorkspacesDisplay.prototype = {
             for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
                 let metaWorkspace = global.screen.get_workspace_by_index(w);
                 this._workspaces[w] = new Workspace.Workspace(metaWorkspace);
+
+                let thumbnail = new WorkspaceThumbnail.WorkspaceThumbnail(metaWorkspace);
+                this._workspaceThumbnails[w] = thumbnail;
+                this._thumbnailsBox.add(thumbnail.actor);
             }
+            this._thumbnailIndicator.raise_top();
         } else {
             // Assume workspaces are only removed sequentially
             // (e.g. 2,3,4 - not 2,4,7)
@@ -914,6 +1001,10 @@ WorkspacesDisplay.prototype = {
             // making its exit.
             for (let l = 0; l < lostWorkspaces.length; l++)
                 lostWorkspaces[l].setReactive(false);
+
+            for (let k = removedIndex; k < removedIndex + removedNum; k++)
+                this._workspaceThumbnails[k].destroy();
+            this._workspaceThumbnails.splice(removedIndex, removedNum);
         }
 
         this.workspacesView.updateWorkspaces(oldNumWorkspaces,